#  Copyright (c) 2015-2018 Cisco Systems, Inc.  # noqa: D100
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to
#  deal in the Software without restriction, including without limitation the
#  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
#  sell copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#  DEALINGS IN THE SOFTWARE.
from __future__ import annotations

import collections
import os

from pathlib import Path
from typing import TYPE_CHECKING, Any

import pytest

from molecule import config, util
from molecule.exceptions import MoleculeError
from molecule.provisioner import ansible, ansible_playbooks
from tests.unit.conftest import os_split  # pylint:disable=C0411


if TYPE_CHECKING:
    from unittest.mock import MagicMock, Mock

    from pytest_mock import MockerFixture

    from molecule.provisioner.ansible import Ansible


@pytest.fixture
def _patched_ansible_playbook(mocker: MockerFixture) -> MagicMock:
    m = mocker.patch("molecule.provisioner.ansible_playbook.AnsiblePlaybook")
    m.return_value.execute.return_value = b"patched-ansible-playbook-stdout"

    return m


@pytest.fixture
def _patched_write_inventory(mocker: MockerFixture) -> MagicMock:
    return mocker.patch("molecule.provisioner.ansible.Ansible._write_inventory")


@pytest.fixture
def _patched_remove_vars(mocker: MockerFixture) -> MagicMock:
    return mocker.patch("molecule.provisioner.ansible.Ansible._remove_vars")


@pytest.fixture
def _patched_link_or_update_vars(mocker: MockerFixture) -> MagicMock:
    return mocker.patch("molecule.provisioner.ansible.Ansible._link_or_update_vars")


@pytest.fixture
def _provisioner_section_data():  # type: ignore[no-untyped-def]  # noqa: ANN202
    return {
        "dependency": {"name": "galaxy"},
        "driver": {"name": "default", "options": {"managed": True}},
        "platforms": [
            {"name": "instance-1", "groups": ["foo", "bar"], "children": ["child1"]},
            {"name": "instance-2", "groups": ["baz", "foo"], "children": ["child2"]},
        ],
        "ansible": {
            "cfg": {"defaults": {"foo": "bar"}},
            "env": {
                "FOO": "bar",
            },
            "executor": {
                "backend": "ansible-playbook",
                "args": {
                    "ansible_navigator": [],
                    "ansible_playbook": [],
                },
            },
            "playbooks": {
                "cleanup": "cleanup.yml",
                "create": "create.yml",
                "converge": "converge.yml",
                "destroy": "destroy.yml",
                "prepare": "prepare.yml",
                "side_effect": "side_effect.yml",
                "verify": "verify.yml",
            },
        },
        "provisioner": {
            "name": "ansible",
            "connection_options": {"foo": "bar"},
            "options": {"foo": "bar", "become": True, "v": True},
            "inventory": {
                "hosts": {
                    "all": {
                        "hosts": {"extra-host-01": {}},
                        "children": {"extra-group": {"hosts": ["extra-host-01"]}},
                    },
                },
                "host_vars": {
                    "instance-1": [{"foo": "bar"}],
                    "localhost": [{"foo": "baz"}],
                },
                "group_vars": {
                    "example_group1": [{"foo": "bar"}],
                    "example_group2": [{"foo": "bar"}],
                },
            },
        },
        "scenario": {"name": "default"},
        "verifier": {"name": "ansible"},
    }


@pytest.fixture(name="instance")
def fixture_instance(
    _provisioner_section_data: dict[str, Any],
    config_instance: config.Config,
) -> ansible.Ansible:
    """Create a provisioner instance.

    Args:
        _provisioner_section_data: A dictionary containing the provisioner section data.
        config_instance: An instance of a Molecule config.
    """
    config_instance.scenario.results.add_action_result("verify")
    return ansible.Ansible(config_instance)


def test_profisioner_config_private_member(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    assert isinstance(instance._config, config.Config)


def test_default_config_options_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = {
        "defaults": {
            "display_failed_stderr": True,
            "forks": 50,
            "host_key_checking": False,
            # https://docs.ansible.com/projects/ansible/devel/reference_appendices/interpreter_discovery.html
            "interpreter_python": "auto_silent",
            "nocows": 1,
            "retry_files_enabled": False,
        },
        "ssh_connection": {
            "control_path": "%(directory)s/%%h-%%p-%%r",
            "scp_if_ssh": True,
        },
    }

    assert x == instance.default_config_options


def test_provisioner_default_options_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    assert instance.default_options == {"skip-tags": "molecule-notest,notest"}


def test_ansible_default_env_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = instance._config.provisioner.config_file

    assert x == instance.default_env["ANSIBLE_CONFIG"]
    assert "MOLECULE_FILE" in instance.default_env
    assert "MOLECULE_INVENTORY_FILE" in instance.default_env
    assert "MOLECULE_SCENARIO_DIRECTORY" in instance.default_env
    assert "MOLECULE_INSTANCE_CONFIG" in instance.default_env
    assert "ANSIBLE_CONFIG" in instance.env


def test_provisioner_name_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    assert instance.name == "ansible"


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_config_options_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = {
        "defaults": {
            "display_failed_stderr": True,
            "foo": "bar",
            "forks": 50,
            "host_key_checking": False,
            "interpreter_python": "auto_silent",
            "nocows": 1,
            "retry_files_enabled": False,
        },
        "ssh_connection": {
            "control_path": "%(directory)s/%%h-%%p-%%r",
            "scp_if_ssh": True,
        },
    }

    assert x == instance.config_options


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_ansible_options_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = {"become": True, "foo": "bar", "v": True, "skip-tags": "molecule-notest,notest"}

    assert x == instance.options


def test_ansible_options_property_does_not_merge(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    for action in ["create", "destroy"]:
        instance._config.action = action

        assert instance.options == {"skip-tags": "molecule-notest,notest"}


def test_provisioner_ansible_options_property_handles_cli_args(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    instance._config.args = {"debug": True}
    x = {
        "vvv": True,
        "become": True,
        "diff": True,
        "skip-tags": "molecule-notest,notest",
    }

    assert x == instance.options


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_provisioner_env_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = instance._config.provisioner.config_file

    assert x == instance.env["ANSIBLE_CONFIG"]
    assert instance.env["FOO"] == "bar"


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_host_vars_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = {"instance-1": [{"foo": "bar"}], "localhost": [{"foo": "baz"}]}

    assert x == instance.host_vars


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_group_vars_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = {"example_group1": [{"foo": "bar"}], "example_group2": [{"foo": "bar"}]}

    assert x == instance.group_vars


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_hosts_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    hosts = {
        "all": {
            "hosts": {"extra-host-01": {}},
            "children": {"extra-group": {"hosts": ["extra-host-01"]}},
        },
    }

    assert hosts == instance.hosts


def test_links_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    assert instance.links == {}


def test_inventory_directory_property(instance: Ansible) -> None:
    """Test the inventory_directory property.

    Args:
        instance: Ansible provisioner instance.
    """
    x = Path(instance._config.scenario.ephemeral_directory, "inventory")
    assert str(x) == instance.inventory_directory


def test_inventory_file_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = os.path.join(  # noqa: PTH118
        instance._config.scenario.inventory_directory,
        "ansible_inventory.yml",
    )

    assert x == instance.inventory_file


def test_config_file_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = os.path.join(instance._config.scenario.ephemeral_directory, "ansible.cfg")  # noqa: PTH118

    assert x == instance.config_file


def test_playbooks_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    assert isinstance(instance.playbooks, ansible_playbooks.AnsiblePlaybooks)


def test_provisioner_directory_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    result = instance.directory
    parts = os_split(result)

    assert parts[-3:] == ("molecule", "provisioner", "ansible")


def test_playbooks_cleaned_property_is_optional(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    assert instance.playbooks.cleanup is None


def test_playbooks_converge_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    x = os.path.join(instance._config.scenario.directory, "converge.yml")  # noqa: PTH118

    assert x == instance.playbooks.converge


def test_playbooks_side_effect_property(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    assert instance.playbooks.side_effect is None


def test_check(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.check()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.converge,
        instance._config,
        verify=False,
    )
    _patched_ansible_playbook.return_value.add_cli_arg.assert_called_once_with(
        "check",
        value=True,
    )
    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_converge(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    result = instance.converge()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.converge,
        instance._config,
        verify=False,
    )
    # NOTE(retr0h): This is not the true return type.  This is a mock return
    #               which didn't go through str.decode().
    assert result == b"patched-ansible-playbook-stdout"  # type: ignore[comparison-overlap]

    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_converge_with_playbook(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    result = instance.converge("playbook")

    _patched_ansible_playbook.assert_called_once_with(
        "playbook",
        instance._config,
        verify=False,
    )
    # NOTE(retr0h): This is not the true return type.  This is a mock return
    #               which didn't go through str.decode().
    assert result == b"patched-ansible-playbook-stdout"  # type: ignore[comparison-overlap]

    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_cleanup(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.cleanup()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.cleanup,
        instance._config,
        verify=False,
    )
    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_destroy(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.destroy()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.destroy,
        instance._config,
        verify=False,
    )
    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_side_effect(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.side_effect()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.side_effect,
        instance._config,
        verify=False,
    )
    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_create(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.create()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.create,
        instance._config,
        verify=False,
    )
    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_prepare(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.prepare()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.prepare,
        instance._config,
        verify=False,
    )
    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_syntax(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.syntax()

    assert instance._config.provisioner

    _patched_ansible_playbook.assert_called_once_with(
        instance._config.provisioner.playbooks.converge,
        instance._config,
        verify=False,
    )
    _patched_ansible_playbook.return_value.add_cli_arg.assert_called_once_with(
        "syntax-check",
        value=True,
    )
    _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_verify(  # noqa: D103
    instance: ansible.Ansible,
    mocker: MockerFixture,
    _patched_ansible_playbook: Mock,  # noqa: PT019
) -> None:
    instance.verify()

    assert instance._config.provisioner

    if instance._config.provisioner.playbooks.verify:
        _patched_ansible_playbook.assert_called_once_with(
            instance._config.provisioner.playbooks.verify,
            instance._config,
        )
        _patched_ansible_playbook.return_value.execute.assert_called_once_with()


def test_ansible_write_config(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    instance.write_config()

    assert os.path.isfile(instance.config_file)  # noqa: PTH113


def test_manage_inventory(  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    instance,
    _patched_write_inventory,  # noqa: PT019
    _patched_remove_vars,  # noqa: PT019
    patched_add_or_update_vars,
    _patched_link_or_update_vars,  # noqa: PT019
):
    instance.manage_inventory()

    _patched_write_inventory.assert_called_once_with()
    _patched_remove_vars.assert_called_once_with()
    patched_add_or_update_vars.assert_called_once_with()
    assert not _patched_link_or_update_vars.called


def test_manage_inventory_with_links(  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    instance,
    _patched_write_inventory,  # noqa: PT019
    _patched_remove_vars,  # noqa: PT019
    patched_add_or_update_vars,
    _patched_link_or_update_vars,  # noqa: PT019
):
    c = instance._config.config
    c["provisioner"]["inventory"]["links"] = {"foo": "bar"}
    instance.manage_inventory()

    _patched_write_inventory.assert_called_once_with()
    _patched_remove_vars.assert_called_once_with()
    assert not patched_add_or_update_vars.called
    _patched_link_or_update_vars.assert_called_once_with()


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_add_or_update_vars(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    inventory_dir = instance._config.scenario.inventory_directory

    host_vars_directory = os.path.join(inventory_dir, "host_vars")  # noqa: PTH118
    host_vars = os.path.join(host_vars_directory, "instance-1")  # noqa: PTH118

    instance._add_or_update_vars()

    assert os.path.isdir(host_vars_directory)  # noqa: PTH112
    assert os.path.isfile(host_vars)  # noqa: PTH113

    host_vars_localhost = os.path.join(host_vars_directory, "localhost")  # noqa: PTH118
    assert os.path.isfile(host_vars_localhost)  # noqa: PTH113

    group_vars_directory = os.path.join(inventory_dir, "group_vars")  # noqa: PTH118
    group_vars_1 = os.path.join(group_vars_directory, "example_group1")  # noqa: PTH118
    group_vars_2 = os.path.join(group_vars_directory, "example_group2")  # noqa: PTH118

    assert os.path.isdir(group_vars_directory)  # noqa: PTH112
    assert os.path.isfile(group_vars_1)  # noqa: PTH113
    assert os.path.isfile(group_vars_2)  # noqa: PTH113

    hosts = os.path.join(inventory_dir, "hosts")  # noqa: PTH118
    assert os.path.isfile(hosts)  # noqa: PTH113
    assert util.safe_load_file(hosts) == instance.hosts


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_add_or_update_vars_without_host_vars(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    c = instance._config.config
    c["provisioner"]["inventory"]["host_vars"] = {}
    inventory_dir = instance._config.scenario.inventory_directory

    host_vars_directory = os.path.join(inventory_dir, "host_vars")  # noqa: PTH118
    host_vars = os.path.join(host_vars_directory, "instance-1")  # noqa: PTH118

    instance._add_or_update_vars()

    assert not os.path.isdir(host_vars_directory)  # noqa: PTH112
    assert not os.path.isfile(host_vars)  # noqa: PTH113

    host_vars_localhost = os.path.join(host_vars_directory, "localhost")  # noqa: PTH118
    assert not os.path.isfile(host_vars_localhost)  # noqa: PTH113

    group_vars_directory = os.path.join(inventory_dir, "group_vars")  # noqa: PTH118
    group_vars_1 = os.path.join(group_vars_directory, "example_group1")  # noqa: PTH118
    group_vars_2 = os.path.join(group_vars_directory, "example_group2")  # noqa: PTH118

    assert os.path.isdir(group_vars_directory)  # noqa: PTH112
    assert os.path.isfile(group_vars_1)  # noqa: PTH113
    assert os.path.isfile(group_vars_2)  # noqa: PTH113

    hosts = os.path.join(inventory_dir, "hosts")  # noqa: PTH118
    assert os.path.isfile(hosts)  # noqa: PTH113
    assert util.safe_load_file(hosts) == instance.hosts


def test_add_or_update_vars_does_not_create_vars(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    c = instance._config.config
    c["provisioner"]["inventory"]["hosts"] = {}
    c["provisioner"]["inventory"]["host_vars"] = {}
    c["provisioner"]["inventory"]["group_vars"] = {}
    inventory_dir = instance._config.scenario.inventory_directory

    hosts = os.path.join(inventory_dir, "hosts")  # noqa: PTH118
    host_vars_directory = os.path.join(inventory_dir, "host_vars")  # noqa: PTH118
    group_vars_directory = os.path.join(inventory_dir, "group_vars")  # noqa: PTH118

    instance._add_or_update_vars()

    assert not os.path.isdir(host_vars_directory)  # noqa: PTH112
    assert not os.path.isdir(group_vars_directory)  # noqa: PTH112
    assert not os.path.isfile(hosts)  # noqa: PTH113


@pytest.mark.parametrize(
    "config_instance",
    ["_provisioner_section_data"],  # noqa: PT007
    indirect=True,
)
def test_remove_vars(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    inventory_dir = instance._config.scenario.inventory_directory

    hosts = os.path.join(inventory_dir, "hosts")  # noqa: PTH118
    host_vars_directory = os.path.join(inventory_dir, "host_vars")  # noqa: PTH118
    host_vars = os.path.join(host_vars_directory, "instance-1")  # noqa: PTH118

    instance._add_or_update_vars()
    assert os.path.isfile(hosts)  # noqa: PTH113
    assert os.path.isdir(host_vars_directory)  # noqa: PTH112
    assert os.path.isfile(host_vars)  # noqa: PTH113

    host_vars_localhost = os.path.join(host_vars_directory, "localhost")  # noqa: PTH118
    assert os.path.isfile(host_vars_localhost)  # noqa: PTH113

    group_vars_directory = os.path.join(inventory_dir, "group_vars")  # noqa: PTH118
    group_vars_1 = os.path.join(group_vars_directory, "example_group1")  # noqa: PTH118
    group_vars_2 = os.path.join(group_vars_directory, "example_group2")  # noqa: PTH118

    assert os.path.isdir(group_vars_directory)  # noqa: PTH112
    assert os.path.isfile(group_vars_1)  # noqa: PTH113
    assert os.path.isfile(group_vars_2)  # noqa: PTH113

    instance._remove_vars()

    assert not os.path.isfile(hosts)  # noqa: PTH113
    assert not os.path.isdir(host_vars_directory)  # noqa: PTH112
    assert not os.path.isdir(group_vars_directory)  # noqa: PTH112


def test_remove_vars_symlinks(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    inventory_dir = instance._config.scenario.inventory_directory

    source_group_vars = os.path.join(inventory_dir, os.path.pardir, "group_vars")  # noqa: PTH118
    target_group_vars = os.path.join(inventory_dir, "group_vars")  # noqa: PTH118
    path = Path(source_group_vars)
    path.parent.mkdir(parents=True, exist_ok=True)
    if not path.is_symlink():
        if path.exists():
            path.unlink()
        path.symlink_to(target_group_vars)

    instance._remove_vars()

    assert not os.path.lexists(target_group_vars)


def test_link_vars(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    c = instance._config.config
    c["provisioner"]["inventory"]["links"] = {
        "hosts": "../hosts",
        "group_vars": "../group_vars",
        "host_vars": "../host_vars",
    }
    inventory_dir = instance._config.scenario.inventory_directory
    scenario_dir = instance._config.scenario.directory
    source_hosts = os.path.join(scenario_dir, os.path.pardir, "hosts")  # noqa: PTH118
    target_hosts = os.path.join(inventory_dir, "hosts")  # noqa: PTH118
    source_group_vars = os.path.join(scenario_dir, os.path.pardir, "group_vars")  # noqa: PTH118
    target_group_vars = os.path.join(inventory_dir, "group_vars")  # noqa: PTH118
    source_host_vars = os.path.join(scenario_dir, os.path.pardir, "host_vars")  # noqa: PTH118
    target_host_vars = os.path.join(inventory_dir, "host_vars")  # noqa: PTH118

    open(source_hosts, "w").close()  # pylint: disable=consider-using-with  # noqa: PTH123
    os.mkdir(source_group_vars)  # noqa: PTH102
    os.mkdir(source_host_vars)  # noqa: PTH102

    instance._link_or_update_vars()

    assert os.path.lexists(target_hosts)
    assert os.path.lexists(target_group_vars)
    assert os.path.lexists(target_host_vars)


def test_link_vars_raises_when_source_not_found(instance: ansible.Ansible, caplog):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    c = instance._config.config
    c["provisioner"]["inventory"]["links"] = {"foo": "../bar"}

    with pytest.raises(MoleculeError) as e:
        instance._link_or_update_vars()

    assert e.value.code == 1

    source = os.path.join(  # noqa: PTH118
        instance._config.scenario.directory,
        os.path.pardir,
        "bar",
    )
    msg = f"The source path '{source}' does not exist."
    assert msg in caplog.text


def test_verify_inventory(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    instance._verify_inventory()


def test_vivify(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    d = instance._vivify()
    d["bar"]["baz"] = "qux"

    assert str(d["bar"]["baz"]) == "qux"


def test_default_to_regular(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    d = collections.defaultdict()  # type: ignore[var-annotated]
    assert isinstance(d, collections.defaultdict)

    d = instance._default_to_regular(d)
    assert isinstance(d, dict)


def test_get_plugin_directory(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    result = instance._get_plugin_directory()
    parts = os_split(result)

    assert parts[-4:] == ("molecule", "provisioner", "ansible", "plugins")


def test_absolute_path_for(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    env = {"foo": "foo:bar"}
    x = ":".join(
        [
            os.path.join(instance._config.scenario.directory, "foo"),  # noqa: PTH118
            os.path.join(instance._config.scenario.directory, "bar"),  # noqa: PTH118
        ],
    )

    assert x == instance._absolute_path_for(env, "foo")


def test_absolute_path_for_raises_with_missing_key(instance):  # type: ignore[no-untyped-def]  # noqa: ANN201, D103
    env = {"foo": "foo:bar"}

    with pytest.raises(KeyError):
        instance._absolute_path_for(env, "invalid")


# Test ansible section integration with provisioner


def test_ansible_args_property_with_ansible_playbook_backend() -> None:
    """Test ansible_args property with ansible-playbook backend (forward-looking config)."""
    c = config.Config(molecule_file="")
    c.config = {
        "ansible": {
            "executor": {
                "backend": "ansible-playbook",
                "args": {"ansible_playbook": ["--forks=20"], "ansible_navigator": []},
            },
            "cfg": {},
            "env": {},
            "playbooks": {},
        },
        "provisioner": {"name": "ansible"},
        "driver": {"name": "default"},
        "platforms": [],
    }

    instance = ansible.Ansible(c)
    assert instance.ansible_args == ["--forks=20"]


def test_ansible_args_property_with_ansible_navigator_backend() -> None:
    """Test ansible_args property with ansible-navigator backend (forward-looking config)."""
    c = config.Config(molecule_file="")
    c.config = {
        "ansible": {
            "executor": {
                "backend": "ansible-navigator",
                "args": {
                    "ansible_navigator": ["--mode=stdout"],
                    "ansible_playbook": ["--forks=10"],
                },
            },
            "cfg": {},
            "env": {},
            "playbooks": {},
        },
        "provisioner": {"name": "ansible"},
        "driver": {"name": "default"},
        "platforms": [],
    }

    instance = ansible.Ansible(c)
    # Should get navigator args + ["--"] + playbook args
    assert instance.ansible_args == ["--mode=stdout", "--", "--forks=10"]


def test_config_options_property_uses_forward_looking_config() -> None:
    """Test config_options property with forward-looking ansible config structure."""
    c = config.Config(molecule_file="")
    c.config = {
        "ansible": {
            "cfg": {"defaults": {"deprecation_warnings": False}},
            "executor": {
                "backend": "ansible-playbook",
                "args": {"ansible_playbook": [], "ansible_navigator": []},
            },
            "env": {},
            "playbooks": {},
        },
        "provisioner": {"name": "ansible"},
        "driver": {"name": "default"},
        "platforms": [],
    }

    instance = ansible.Ansible(c)
    result = instance.config_options

    # Should get the ansible.cfg value merged with defaults
    assert result["defaults"]["deprecation_warnings"] is False
    # Should still have default config options
    assert "forks" in result["defaults"]
    assert "host_key_checking" in result["defaults"]


def test_config_options_property_merges_ansible_cfg_with_base() -> None:
    """Test config_options property merges ansible.cfg with base config."""
    c = config.Config(molecule_file="")
    c.config = {
        "ansible": {
            "cfg": {
                "defaults": {"deprecation_warnings": False},
                "ssh_connection": {"scp_if_ssh": True},
            },
            "executor": {
                "backend": "ansible-playbook",
                "args": {"ansible_playbook": [], "ansible_navigator": []},
            },
            "env": {},
            "playbooks": {},
        },
        "provisioner": {"name": "ansible"},
        "driver": {"name": "default"},
        "platforms": [],
    }

    instance = ansible.Ansible(c)
    result = instance.config_options

    # Should have ansible.cfg settings merged with defaults
    assert result["defaults"]["deprecation_warnings"] is False
    assert result["ssh_connection"]["scp_if_ssh"] is True
    # Should still have default config options merged in
    assert "forks" in result["defaults"]
